package sparrow.etl.core.util.tar;

import java.io.File;
import java.util.Date;

import sparrow.etl.core.exception.InvalidHeaderException;


/**
 *
 * <p>Title: </p>
 * <p>Description: </p>
 * <p>Copyright: Copyright (c) 2004</p>
 * <p>Company: </p>
 * @author not attributable
 * @version 1.0
 */
public
    class TarEntry
    extends Object
    implements Cloneable {
  /**
   * If this entry represents a File, this references it.
   */
  protected File file;

  /**
   * This is the entry's header information.
   */
  protected TarHeader header;

  /**
   * Set to true if this is a "old-unix" format entry.
   */
  protected boolean unixFormat;

  /**
   * Set to true if this is a 'ustar' format entry.
   */
  protected boolean ustarFormat;

  /**
   * Set to true if this is a GNU 'ustar' format entry.
   */
  protected boolean gnuFormat;

  /**
   * The default constructor is protected for use only by subclasses.
   */
  protected
      TarEntry() {
  }

  /**
   * Construct an entry with only a name. This allows the programmer
   * to construct the entry's header "by hand". File is set to null.
   */
  public
      TarEntry(String name) {
    this.initialize();
    this.nameTarHeader(this.header, name);
  }

  /**
   * Construct an entry for a file. File is set to file, and the
   * header is constructed from information from the file.
   *
   * @param file The file that the entry represents.
   */
  public
      TarEntry(File file) throws InvalidHeaderException {
    this.initialize();
    this.getFileTarHeader(this.header, file);
  }

  /**
   * Construct an entry from an archive's header bytes. File is set
   * to null.
   *
   * @param headerBuf The header bytes from a tar archive entry.
   */
  public
      TarEntry(byte[] headerBuf) throws InvalidHeaderException {
    this.initialize();
    this.parseTarHeader(this.header, headerBuf);
  }

  /**
   * Initialization code common to all constructors.
   */
  private void
      initialize() {
    this.file = null;
    this.header = new TarHeader();

    this.gnuFormat = false;
    this.ustarFormat = true; // REVIEW What we prefer to use...
    this.unixFormat = false;
  }

  /**
   * Clone the entry.
   */
  public Object
      clone() {
    TarEntry entry = null;

    try {
      entry = (TarEntry)super.clone();

      if (this.header != null) {
        entry.header = (TarHeader)this.header.clone();
      }

      if (this.file != null) {
        entry.file = new File(this.file.getAbsolutePath());
      }
    }
    catch (CloneNotSupportedException ex) {
      ex.printStackTrace(System.err);
    }

    return entry;
  }

  /**
   * Returns true if this entry's header is in "ustar" format.
   *
   * @return True if the entry's header is in "ustar" format.
   */
  public boolean
      isUSTarFormat() {
    return this.ustarFormat;
  }

  /**
   * Sets this entry's header format to "ustar".
   */
  public void
      setUSTarFormat() {
    this.ustarFormat = true;
    this.gnuFormat = false;
    this.unixFormat = false;
  }

  /**
   * Returns true if this entry's header is in the GNU 'ustar' format.
   *
   * @return True if the entry's header is in the GNU 'ustar' format.
   */
  public boolean
      isGNUTarFormat() {
    return this.gnuFormat;
  }

  /**
   * Sets this entry's header format to GNU "ustar".
   */
  public void
      setGNUTarFormat() {
    this.gnuFormat = true;
    this.ustarFormat = false;
    this.unixFormat = false;
  }

  /**
   * Returns true if this entry's header is in the old "unix-tar" format.
   *
   * @return True if the entry's header is in the old "unix-tar" format.
   */
  public boolean
      isUnixTarFormat() {
    return this.unixFormat;
  }

  /**
   * Sets this entry's header format to "unix-style".
   */
  public void
      setUnixTarFormat() {
    this.unixFormat = true;
    this.ustarFormat = false;
    this.gnuFormat = false;
  }

  /**
   * Determine if the two entries are equal. Equality is determined
   * by the header names being equal.
   *
   * @return it Entry to be checked for equality.
   * @return True if the entries are equal.
   */
  public boolean
      equals(TarEntry it) {
    return
        this.header.name.toString().equals
        (it.header.name.toString());
  }

  /**
   * Determine if the given entry is a descendant of this entry.
   * Descendancy is determined by the name of the descendant
   * starting with this entry's name.
   *
   * @param desc Entry to be checked as a descendent of this.
   * @return True if entry is a descendant of this.
   */
  public boolean
      isDescendent(TarEntry desc) {
    return
        desc.header.name.toString().startsWith
        (this.header.name.toString());
  }

  /**
   * Get this entry's header.
   *
   * @return This entry's TarHeader.
   */
  public TarHeader
      getHeader() {
    return this.header;
  }

  /**
   * Get this entry's name.
   *
   * @return This entry's name.
   */
  public String
      getName() {
    return this.header.name.toString();
  }

  /**
   * Set this entry's name.
   *
   * @param name This entry's new name.
   */
  public void
      setName(String name) {
    this.header.name =
        new StringBuffer(name);
  }

  /**
   * Get this entry's user id.
   *
   * @return This entry's user id.
   */
  public int
      getUserId() {
    return this.header.userId;
  }

  /**
   * Set this entry's user id.
   *
   * @param userId This entry's new user id.
   */
  public void
      setUserId(int userId) {
    this.header.userId = userId;
  }

  /**
   * Get this entry's group id.
   *
   * @return This entry's group id.
   */
  public int
      getGroupId() {
    return this.header.groupId;
  }

  /**
   * Set this entry's group id.
   *
   * @param groupId This entry's new group id.
   */
  public void
      setGroupId(int groupId) {
    this.header.groupId = groupId;
  }

  /**
   * Get this entry's user name.
   *
   * @return This entry's user name.
   */
  public String
      getUserName() {
    return this.header.userName.toString();
  }

  /**
   * Set this entry's user name.
   *
   * @param userName This entry's new user name.
   */
  public void
      setUserName(String userName) {
    this.header.userName =
        new StringBuffer(userName);
  }

  /**
   * Get this entry's group name.
   *
   * @return This entry's group name.
   */
  public String
      getGroupName() {
    return this.header.groupName.toString();
  }

  /**
   * Set this entry's group name.
   *
   * @param groupName This entry's new group name.
   */
  public void
      setGroupName(String groupName) {
    this.header.groupName =
        new StringBuffer(groupName);
  }

  /**
   * Convenience method to set this entry's group and user ids.
   *
   * @param userId This entry's new user id.
   * @param groupId This entry's new group id.
   */
  public void
      setIds(int userId, int groupId) {
    this.setUserId(userId);
    this.setGroupId(groupId);
  }

  /**
   * Convenience method to set this entry's group and user names.
   *
   * @param userName This entry's new user name.
   * @param groupName This entry's new group name.
   */
  public void
      setNames(String userName, String groupName) {
    this.setUserName(userName);
    this.setGroupName(groupName);
  }

  /**
   * Set this entry's modification time. The parameter passed
   * to this method is in "Java time".
   *
   * @param time This entry's new modification time.
   */
  public void
      setModTime(long time) {
    this.header.modTime = time / 1000;
  }

  /**
   * Set this entry's modification time.
   *
   * @param time This entry's new modification time.
   */
  public void
      setModTime(Date time) {
    this.header.modTime = time.getTime() / 1000;
  }

  /**
   * Set this entry's modification time.
   *
   * @param time This entry's new modification time.
   */
  public Date
      getModTime() {
    return new Date(this.header.modTime * 1000);
  }

  /**
   * Get this entry's file.
   *
   * @return This entry's file.
   */
  public File
      getFile() {
    return this.file;
  }

  /**
   * Get this entry's file size.
   *
   * @return This entry's file size.
   */
  public long
      getSize() {
    return this.header.size;
  }

  /**
   * Set this entry's file size.
   *
   * @param size This entry's new file size.
   */
  public void
      setSize(long size) {
    this.header.size = size;
  }

  /**
   * Return whether or not this entry represents a directory.
   *
   * @return True if this entry is a directory.
   */
  public boolean
      isDirectory() {
    if (this.file != null) {
      return this.file.isDirectory();
    }

    if (this.header != null) {
      if (this.header.linkFlag == TarHeader.LF_DIR) {
        return true;
      }

      if (this.header.name.toString().endsWith("/")) {
        return true;
      }
    }

    return false;
  }

  /**
   * Fill in a TarHeader with information from a File.
   *
   * @param hdr The TarHeader to fill in.
   * @param file The file from which to get the header information.
   */
  public void
      getFileTarHeader(TarHeader hdr, File file) throws InvalidHeaderException {
    this.file = file;

    String name = file.getPath();
    String osname = System.getProperty("os.name");
    if (osname != null) {
      // Strip off drive letters!
      // REVIEW Would a better check be "(File.separator == '\')"?

      // String Win32Prefix = "Windows";
      // String prefix = osname.substring( 0, Win32Prefix.length() );
      // if ( prefix.equalsIgnoreCase( Win32Prefix ) )

      // if ( File.separatorChar == '\\' )

      // Windows OS check was contributed by
      // Patrick Beard <beard@netscape.com>
      String Win32Prefix = "windows";
      if (osname.toLowerCase().startsWith(Win32Prefix)) {
        if (name.length() > 2) {
          char ch1 = name.charAt(0);
          char ch2 = name.charAt(1);
          if (ch2 == ':'
              && ( (ch1 >= 'a' && ch1 <= 'z')
                  || (ch1 >= 'A' && ch1 <= 'Z'))) {
            name = name.substring(2);
          }
        }
      }
    }

    name = name.replace(File.separatorChar, '/');

    // No absolute pathnames
    // Windows (and Posix?) paths can start with "\\NetworkDrive\",
    // so we loop on starting /'s.

    for (; name.startsWith("/"); ) {
      name = name.substring(1);

    }
    hdr.linkName = new StringBuffer("");

    hdr.name = new StringBuffer(name);

    if (file.isDirectory()) {
      hdr.size = 0;
      hdr.mode = 040755;
      hdr.linkFlag = TarHeader.LF_DIR;
      if (hdr.name.charAt(hdr.name.length() - 1) != '/') {
        hdr.name.append("/");
      }
    }
    else {
      hdr.size = file.length();
      hdr.mode = 0100644;
      hdr.linkFlag = TarHeader.LF_NORMAL;
    }

    // UNDONE When File lets us get the userName, use it!

    hdr.modTime = file.lastModified() / 1000;
    hdr.checkSum = 0;
    hdr.devMajor = 0;
    hdr.devMinor = 0;
  }

  /**
   * If this entry represents a file, and the file is a directory, return
   * an array of TarEntries for this entry's children.
   *
   * @return An array of TarEntry's for this entry's children.
   */
  public TarEntry[]
      getDirectoryEntries() throws InvalidHeaderException {
    if (this.file == null
        || !this.file.isDirectory()) {
      return new TarEntry[0];
    }

    String[] list = this.file.list();

    TarEntry[] result = new TarEntry[list.length];

    for (int i = 0; i < list.length; ++i) {
      result[i] =
          new TarEntry
          (new File(this.file, list[i]));
    }

    return result;
  }

  /**
   * Compute the checksum of a tar entry header.
   *
   * @param buf The tar entry's header buffer.
   * @return The computed checksum.
   */
  public long
      computeCheckSum(byte[] buf) {
    long sum = 0;

    for (int i = 0; i < buf.length; ++i) {
      sum += 255 & buf[i];
    }

    return sum;
  }

  /**
   * Write an entry's header information to a header buffer.
   * This method can throw an InvalidHeaderException
   *
   * @param outbuf The tar entry header buffer to fill in.
   * @throws InvalidHeaderException If the name will not fit in the header.
   */
  public void
      writeEntryHeader(byte[] outbuf) throws InvalidHeaderException {
    int offset = 0;

    if (this.isUnixTarFormat()) {
      if (this.header.name.length() > 100) {
        throw new InvalidHeaderException
            ("file path is greater than 100 characters, "
             + this.header.name);
      }
    }

    offset = TarHeader.getFileNameBytes(this.header.name.toString(), outbuf);

    offset = TarHeader.getOctalBytes
        (this.header.mode, outbuf, offset, TarHeader.MODELEN);

    offset = TarHeader.getOctalBytes
        (this.header.userId, outbuf, offset, TarHeader.UIDLEN);

    offset = TarHeader.getOctalBytes
        (this.header.groupId, outbuf, offset, TarHeader.GIDLEN);

    long size = this.header.size;

    offset = TarHeader.getLongOctalBytes
        (size, outbuf, offset, TarHeader.SIZELEN);

    offset = TarHeader.getLongOctalBytes
        (this.header.modTime, outbuf, offset, TarHeader.MODTIMELEN);

    int csOffset = offset;
    for (int c = 0; c < TarHeader.CHKSUMLEN; ++c) {
      outbuf[offset++] = (byte) ' ';

    }
    outbuf[offset++] = this.header.linkFlag;

    offset = TarHeader.getNameBytes
        (this.header.linkName, outbuf, offset, TarHeader.NAMELEN);

    if (this.unixFormat) {
      for (int i = 0; i < TarHeader.MAGICLEN; ++i) {
        outbuf[offset++] = 0;
      }
    }
    else {
      offset = TarHeader.getNameBytes
          (this.header.magic, outbuf, offset, TarHeader.MAGICLEN);
    }

    offset = TarHeader.getNameBytes
        (this.header.userName, outbuf, offset, TarHeader.UNAMELEN);

    offset = TarHeader.getNameBytes
        (this.header.groupName, outbuf, offset, TarHeader.GNAMELEN);

    offset = TarHeader.getOctalBytes
        (this.header.devMajor, outbuf, offset, TarHeader.DEVLEN);

    offset = TarHeader.getOctalBytes
        (this.header.devMinor, outbuf, offset, TarHeader.DEVLEN);

    for (; offset < outbuf.length; ) {
      outbuf[offset++] = 0;

    }
    long checkSum = this.computeCheckSum(outbuf);

    TarHeader.getCheckSumOctalBytes
        (checkSum, outbuf, csOffset, TarHeader.CHKSUMLEN);
  }

  /**
   * Parse an entry's TarHeader information from a header buffer.
   *
   * Old unix-style code contributed by David Mehringer <dmehring@astro.uiuc.edu>.
   *
   * @param hdr The TarHeader to fill in from the buffer information.
   * @param header The tar entry header buffer to get information from.
   */
  public void
      parseTarHeader(TarHeader hdr, byte[] headerBuf) throws
      InvalidHeaderException {
    int offset = 0;

    //
    // NOTE Recognize archive header format.
    //
    if (headerBuf[257] == 0
        && headerBuf[258] == 0
        && headerBuf[259] == 0
        && headerBuf[260] == 0
        && headerBuf[261] == 0) {
      this.unixFormat = true;
      this.ustarFormat = false;
      this.gnuFormat = false;
    }
    else if (headerBuf[257] == 'u'
             && headerBuf[258] == 's'
             && headerBuf[259] == 't'
             && headerBuf[260] == 'a'
             && headerBuf[261] == 'r'
             && headerBuf[262] == 0) {
      this.ustarFormat = true;
      this.gnuFormat = false;
      this.unixFormat = false;
    }
    else if (headerBuf[257] == 'u'
             && headerBuf[258] == 's'
             && headerBuf[259] == 't'
             && headerBuf[260] == 'a'
             && headerBuf[261] == 'r'
             && headerBuf[262] != 0
             && headerBuf[263] != 0) {
      // REVIEW
      this.gnuFormat = true;
      this.unixFormat = false;
      this.ustarFormat = false;
    }
    else {
      StringBuffer buf = new StringBuffer(128);

      buf.append("header magic is not 'ustar' or unix-style zeros, it is '");
      buf.append(headerBuf[257]);
      buf.append(headerBuf[258]);
      buf.append(headerBuf[259]);
      buf.append(headerBuf[260]);
      buf.append(headerBuf[261]);
      buf.append(headerBuf[262]);
      buf.append(headerBuf[263]);
      buf.append("', or (dec) ");
      buf.append( (int) headerBuf[257]);
      buf.append(", ");
      buf.append( (int) headerBuf[258]);
      buf.append(", ");
      buf.append( (int) headerBuf[259]);
      buf.append(", ");
      buf.append( (int) headerBuf[260]);
      buf.append(", ");
      buf.append( (int) headerBuf[261]);
      buf.append(", ");
      buf.append( (int) headerBuf[262]);
      buf.append(", ");
      buf.append( (int) headerBuf[263]);

      throw new InvalidHeaderException(buf.toString());
    }

    hdr.name = TarHeader.parseFileName(headerBuf);

    offset = TarHeader.NAMELEN;

    hdr.mode = (int)
        TarHeader.parseOctal(headerBuf, offset, TarHeader.MODELEN);

    offset += TarHeader.MODELEN;

    hdr.userId = (int)
        TarHeader.parseOctal(headerBuf, offset, TarHeader.UIDLEN);

    offset += TarHeader.UIDLEN;

    hdr.groupId = (int)
        TarHeader.parseOctal(headerBuf, offset, TarHeader.GIDLEN);

    offset += TarHeader.GIDLEN;

    hdr.size =
        TarHeader.parseOctal(headerBuf, offset, TarHeader.SIZELEN);

    offset += TarHeader.SIZELEN;

    hdr.modTime =
        TarHeader.parseOctal(headerBuf, offset, TarHeader.MODTIMELEN);

    offset += TarHeader.MODTIMELEN;

    hdr.checkSum = (int)
        TarHeader.parseOctal(headerBuf, offset, TarHeader.CHKSUMLEN);

    offset += TarHeader.CHKSUMLEN;

    hdr.linkFlag = headerBuf[offset++];

    hdr.linkName =
        TarHeader.parseName(headerBuf, offset, TarHeader.NAMELEN);

    offset += TarHeader.NAMELEN;

    if (this.ustarFormat) {
      hdr.magic =
          TarHeader.parseName(headerBuf, offset, TarHeader.MAGICLEN);

      offset += TarHeader.MAGICLEN;

      hdr.userName =
          TarHeader.parseName(headerBuf, offset, TarHeader.UNAMELEN);

      offset += TarHeader.UNAMELEN;

      hdr.groupName =
          TarHeader.parseName(headerBuf, offset, TarHeader.GNAMELEN);

      offset += TarHeader.GNAMELEN;

      hdr.devMajor = (int)
          TarHeader.parseOctal(headerBuf, offset, TarHeader.DEVLEN);

      offset += TarHeader.DEVLEN;

      hdr.devMinor = (int)
          TarHeader.parseOctal(headerBuf, offset, TarHeader.DEVLEN);
    }
    else {
      hdr.devMajor = 0;
      hdr.devMinor = 0;
      hdr.magic = new StringBuffer("");
      hdr.userName = new StringBuffer("");
      hdr.groupName = new StringBuffer("");
    }
  }

  /**
   * Fill in a TarHeader given only the entry's name.
   *
   * @param hdr The TarHeader to fill in.
   * @param name The tar entry name.
   */
  public void
      nameTarHeader(TarHeader hdr, String name) {
    boolean isDir = name.endsWith("/");

    this.gnuFormat = false;
    this.ustarFormat = true;
    this.unixFormat = false;

    hdr.checkSum = 0;
    hdr.devMajor = 0;
    hdr.devMinor = 0;

    hdr.name = new StringBuffer(name);
    hdr.mode = isDir ? 040755 : 0100644;
    hdr.userId = 0;
    hdr.groupId = 0;
    hdr.size = 0;
    hdr.checkSum = 0;

    hdr.modTime =
        (new java.util.Date()).getTime() / 1000;

    hdr.linkFlag =
        isDir ? TarHeader.LF_DIR : TarHeader.LF_NORMAL;

    hdr.linkName = new StringBuffer("");
    hdr.userName = new StringBuffer("");
    hdr.groupName = new StringBuffer("");

    hdr.devMajor = 0;
    hdr.devMinor = 0;
  }

  public String
      toString() {
    StringBuffer result = new StringBuffer(128);
    return result.
        append("[TarEntry name=").
        append(this.getName()).
        append(", isDir=").
        append(this.isDirectory()).
        append(", size=").
        append(this.getSize()).
        append(", userId=").
        append(this.getUserId()).
        append(", user=").
        append(this.getUserName()).
        append(", groupId=").
        append(this.getGroupId()).
        append(", group=").
        append(this.getGroupName()).
        append("]").
        toString();
  }

}
